iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Modern Web

Go into Web!系列 第 8 篇

Day8 | 使用 ORM 與資料庫進行互動

  • 分享至 

  • xImage
  •  

昨天我們使用 driver 與 mysql 進行互動,但在使用的時候會發現,做任何動作都必須使用 raw SQL,這樣可能會有幾個問題產生

  • 在開發網頁時很容易被以 SQL Injection 的方式攻擊
  • 使用不同類型的資料庫全部語法都要改過

因此,我們今天會說明如何使用 ORM 來解決以上的問題。

什麼是 ORM

ORM 為 Object Relation Mapping 的縮寫,翻譯過來就是物件關聯對映。

簡單來說,就是 將程式物件對應到資料庫內容的一種抽象化技術,為什麼要進行抽象化呢,主要的原因為當我們在進行開發時,主要專注在物件的描述即可,不用關注底層是使用何種資料庫,也就是在程式碼不變的情況下我們可以任意切換使用不同的資料庫。

而 ORM 有著其他的優點,接下來會一一的介紹

大量減少重複的 SQL 語句

對於資料庫的操作最常使用的就是 CRUD,也就是所謂的 insert, select, update 與 delete,然而如果在操作不同的 table 時就會重複的使用到這些語法,ORM 這些語法進行封裝,我們只要呼叫對應的方法就可以重複使用了。

安全性

直接使用 SQL 語句來操作資料庫,最常見的資安漏洞就是被使用 SQL Injection 的方式攻擊,SQL Injection 就是利用輸入的字串中夾帶 SQL 語句,導致資料庫在執行時被入侵或是破壞,以下為 SQL Injection 的情境

假設我們要查詢 users 這張 table,將 Id 當作是 Input,因此設計了一個 Select 語法如下

SELECT * FROM users WHERE Id = <id>;

一般而言就是輸入id,但透過 SQL Injection 的方式只需要在輸入的時候多加上 OR 1=1 就可以撈出所有的資料,用這種方式跳過驗證。

ORM 要避免 SQL Injection 的情況出現,多半會使用判斷輸入的字串是否有問題等方式處理,因此,使用 ORM 可以增加整體的安全性是毋庸置疑的。

降低耦合力

使用 ORM 在開發時只要好好的面對物件即可,無需關注到原生的 SQL 需要怎麼下,在轉換不同類型的資料庫時也不用修改程式,可以大幅的降低程式與資料庫之間的耦合力。

講到這裡,相信大家對於 ORM 已經有基礎的理解了,接下來我們開始使用吧!

使用 ORM

目前 golang 有需多開源的 ORM 可以使用,詳細的部分可以參考這裡。
我自己是選用 gorm,主要是因為他的文件非常完整與可以支援熱加載(Eager loading),還可以進行 Auto Migrations,接下來就讓我們開始使用 gorm 吧

安裝

使用 gorm 非常簡單,只要透過 go get 的方法進行安裝,同時也要安裝由 gorm 官方所提供的 driver,這邊以 mysql 為例

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

連線

在使用以前要先進行 import,匯入 gorm 與 mysql driver

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

接著我們將連線的資訊設定為常數

const (
	USERNAME = "demo1"
	PASSWORD = "demo123"
	NETWORK = "tcp"
	SERVER = "127.0.0.1"
	PORT = 3306
	DATABASE = "demo"
)

透過 gorm 連線到資料庫與昨天講的 driver 非常相似,都是透過 dsn 進行連線

dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

檢查資料庫是否連線正常

    panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
}

整體的連線程式會像這樣

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	USERNAME = "demo"
	PASSWORD = "demo123"
	NETWORK  = "tcp"
	SERVER   = "127.0.0.1"
	PORT     = 3306
	DATABASE = "demo"
)

func main() {
	dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE)
	_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
	}
}

執行後如果沒有任何錯誤訊息代表連線成功拉!

建立 Data Model

使用 ORM 顧名思義就是要使用 Object,因此我們先來設定一個名為 user 的 struct

type User struct {
	ID       int64  `json:"id" gorm:"primary_key;auto_increase'"`
	Username string `json:"username"`
	Password string `json:""`
}

要將 struct mapping 到資料庫的內容,必須要使用 gorm 的 tag,tag 詳細內容可以參考這裡。

透過 Data Model 建立 Table

接著我們可以使用 gorm 內建的 AutoMigrate 方法進行 Table 的建立,很多人一定想問,為什麼要使用 AutoMigrate 方法呢,因為使用這個方法就可以在沒有 Table 時新增,Table 有更動時自動更新,因此選用這個方法

if err := db.AutoMigrate(new(User)); err != nil {
    panic("資料庫 Migrate 失敗,原因為 " + err.Error())
}

我們可以將整體修改為這樣

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	USERNAME = "demo"
	PASSWORD = "demo123"
	NETWORK  = "tcp"
	SERVER   = "127.0.0.1"
	PORT     = 3306
	DATABASE = "demo"
)

type User struct {
	ID       int64  `json:"id" gorm:"primary_key;auto_increase'"`
	Username string `json:"username"`
	Password string `json:""`
}

func main() {
	dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
	}

    if err := db.AutoMigrate(new(User)); err != nil {
		panic("資料庫 Migrate 失敗,原因為 " + err.Error())
	}
}

執行出來的結果如果沒有跳出錯誤訊息代表成功的建立 Table 了,我們可以透過任何資料庫管理工具查看結果是否正確。

新增資料

新增資料變得非常的簡單,只要建立一個新的物件後,呼叫 gorm 所提供的 Create 方法即可,我們建立一個名為 CreateUser 的方法

func CreateUser(db *gorm.DB, user *User) error {
	return db.Create(user).Error
}

之後修改 main 方法,建立一個 User 物件後呼叫此方法

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	USERNAME = "demo"
	PASSWORD = "demo123"
	NETWORK  = "tcp"
	SERVER   = "127.0.0.1"
	PORT     = 3306
	DATABASE = "demo"
)

type User struct {
	ID       int64  `json:"id" gorm:"primary_key;auto_increase'"`
	Username string `json:"username"`
	Password string `json:""`
}

func main() {
	dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
	}

    if err := db.AutoMigrate(new(User)); err != nil {
		panic("資料庫 Migrate 失敗,原因為 " + err.Error())
	}
    user := &User{
		Username: "test",
		Password: "test",
	}
    if err := CreateUser(db, user)); err != nil {
		panic("資料庫 Migrate 失敗,原因為 " + err.Error())
	}
}
func CreateUser(db *gorm.DB, user *User) error {
	return db.Create(user).Error
}

執行出來的結果如果沒有跳出錯誤訊息代表成功的建立 User 了,我們可以透過任何資料庫管理工具查看結果是否正確。

查詢資料

查詢的部分更為簡單,gorm 本身有提供 First 與 Find 方法,分別用於查詢一筆資料或是多筆資料,我們使用 First 進行測試,新增一個方法名為 FindUser,將 db 與 id 當作參數,回傳 User

func FindUser(db *gorm.DB, id int64) (*User, error) {
	user := new(User)
	user.ID = id
	err := db.First(&user).Error
	return user, err
}

之後修改 main 方法,設定要查詢 id 為 1

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log"
)

const (
	USERNAME = "demo"
	PASSWORD = "demo123"
	NETWORK  = "tcp"
	SERVER   = "127.0.0.1"
	PORT     = 3306
	DATABASE = "demo"
)

type User struct {
	ID       int64  `json:"id" gorm:"primary_key;auto_increase'"`
	Username string `json:"username"`
	Password string `json:""`
}

func main() {
	dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", USERNAME, PASSWORD, NETWORK, SERVER, PORT, DATABASE)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("使用 gorm 連線 DB 發生錯誤,原因為 " + err.Error())
	}

	if err := db.AutoMigrate(new(User)); err != nil {
		panic("資料庫 Migrate 失敗,原因為 " + err.Error())
	}
	user := &User{
		Username: "test",
		Password: "test",
	}
	if err := CreateUser(db, user);err != nil {
		panic("新增 user 失敗,原因為 " + err.Error())
	}

	if user, err := FindUser(db, 1); err == nil {
		log.Println("查詢到 User 為 ", user)
	} else {
		panic("查詢 user 失敗,原因為 " + err.Error())
	}
}

func CreateUser(db *gorm.DB, user *User) error {
	return db.Create(user).Error
}

func FindUser(db *gorm.DB, id int64) (*User, error) {
	user := new(User)
	user.ID = id
	err := db.First(&user).Error
	return user, err
}

執行出來的結果如果顯示像 2020/09/08 23:59:42 查詢到 User 為 &{1 test test} 代表成功查詢了,我們可以透過任何資料庫管理工具查看結果是否正確。

更多語法可以參考 gorm 的官方文件。

小結

從今天的範例可以看出,使用 orm 可以讓我們的程式更為簡潔,而且單純使用物件的方式就可以與資料庫進行互動,讓我們有更多的精神 focus 在產品的開發上。

在這幾天的文章中可以發現,許多的設定我們都把他寫在程式內,像是前幾天講到的建立網站的 port 以及今天資料庫的相關設定,這樣的方式並不好,在未來程式如果要真正的上線,勢必會放在不同的環境中,不可能針對每一個環境都重新修改程式,因此,明天就讓我們來聊聊 配置檔的處理 吧!

參考


上一篇
Day7 | 使用 GoLang 與資料庫進行互動
下一篇
Day9 | 輕鬆管理程式的設定檔
系列文
Go into Web! 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言